1 /*
2 * Scope: a generic MVC framework.
3 * Copyright (c) 2000-2002, The Scope team
4 * All rights reserved.
5 *
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 *
14 * Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 *
18 * Neither the name "Scope" nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
21 *
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 *
35 *
36 * $Id: BeansPropertyManager.java,v 1.10 2002/09/12 10:51:03 ludovicc Exp $
37 */
38 package org.scopemvc.model.beans;
39
40
41 import java.beans.IndexedPropertyDescriptor;
42 import java.beans.PropertyDescriptor;
43 import java.lang.reflect.InvocationTargetException;
44 import java.lang.reflect.Method;
45 import java.util.Iterator;
46 import java.util.List;
47 import org.apache.commons.logging.Log;
48 import org.apache.commons.logging.LogFactory;
49 import org.scopemvc.core.IntIndexSelector;
50 import org.scopemvc.core.PropertyManager;
51 import org.scopemvc.core.Selector;
52 import org.scopemvc.core.StringIndexSelector;
53 import org.scopemvc.util.Debug;
54 import org.scopemvc.model.util.ArraySelectorIterator;
55 import org.scopemvc.model.util.CompoundSelectorIterator;
56 import org.scopemvc.model.util.IntIndexSelectorIterator;
57
58 /***
59 * <P>
60 *
61 * BeansPropertyManager is a {@link org.scopemvc.core.PropertyManager} that
62 * handles the properties of JavaBean model objects. </P>
63 *
64 * @author <A HREF="mailto:smeyfroi@users.sourceforge.net">Steve Meyfroidt</A>
65 * @created 05 September 2002
66 * @version $Revision: 1.10 $ $Date: 2002/09/12 10:51:03 $
67 * @todo Use commons-beanutils functionality (ludovicc)
68 */
69 public class BeansPropertyManager extends PropertyManager {
70
71 private static final Log LOG = LogFactory.getLog(BeansPropertyManager.class);
72
73
74 /***
75 * <P>
76 *
77 * Return the value of the property identified by the passed {@link
78 * Selector}. If the passed Selector is null, return the model object
79 * itself. </P>
80 *
81 * @param inModel model to get property from
82 * @param inSelector identify the property to be returned or null for the
83 * model object itself.
84 * @return value of selected property
85 * @throws Exception If the value of the property could not be retrieved
86 */
87 public Object get(Object inModel, Selector inSelector) throws Exception {
88 if (LOG.isDebugEnabled()) {
89 LOG.debug("get: " + inModel + ", " + inSelector);
90 }
91
92 if (inModel == null) {
93 // throw new IllegalArgumentException("Can't get from a null model.");
94 return null;
95 }
96
97 if (inSelector == null) {
98 return inModel;
99 }
100
101 try {
102 Accessor accessor = findTerminalAccessor(inModel, inSelector);
103 accessor.traverseProperty();
104 return accessor.model;
105 } catch (NullPropertyException e) {
106 LOG.warn("Could not locate the property using selector " + inSelector + " in model " + inModel);
107 return null;
108 }
109 }
110
111
112 /***
113 * <P>
114 *
115 * Is a property read-only? If the passed Selector is null then is the model
116 * object as a whole read-only? </P> <P>
117 *
118 * Enforcement of the access state must be implemented by the model itself
119 * by using the DynamicReadOnly interface. </P>
120 *
121 * @param inModel model object to test the property on.
122 * @param inSelector The property to test or null to test the whole model
123 * object.
124 * @return whether the property is read-only or not.
125 * @throws Exception If the read-only state of the property could not be
126 * tested
127 * @see DynamicReadOnly
128 */
129 public boolean isReadOnly(Object inModel, Selector inSelector)
130 throws Exception {
131 if (inModel == null) {
132 throw new IllegalArgumentException("Can't ask isReadOnly for a null model.");
133 }
134
135 if (inSelector == null) {
136 return true;
137 // throw new ModelException(inModel, "Can't test read-only for model");
138 }
139
140 Accessor accessor;
141 try {
142 accessor = findTerminalAccessor(inModel, inSelector);
143 } catch (Exception e) {
144 return true;
145 }
146
147 if (accessor.descriptor == null) {
148 // List elements always mutable by default
149 if (accessor.model instanceof DynamicReadOnly) {
150 return ((DynamicReadOnly) accessor.model).isPropertyReadOnly(accessor.selector);
151 }
152 return false;
153 } else if (accessor.isGetterIndexed) {
154 // Indexed setter
155 return (((IndexedPropertyDescriptor) accessor.descriptor).getIndexedWriteMethod() == null);
156 } else {
157 // Normal setter
158 if (accessor.model instanceof DynamicReadOnly) {
159 if (((DynamicReadOnly) accessor.model).isPropertyReadOnly(accessor.selector)) {
160 return true;
161 }
162 }
163 return (accessor.descriptor.getWriteMethod() == null);
164 }
165 }
166
167
168 /***
169 * Return the Class of a property.
170 *
171 * @param inModel model to test the property for.
172 * @param inSelector property to test.
173 * @return Class of property. Never null.
174 * @throws Exception If the class of the property could not be retrieved
175 */
176 public Class getPropertyClass(Object inModel, Selector inSelector)
177 throws Exception {
178 if (inModel == null) {
179 throw new IllegalArgumentException("Can't getPropertyClass for a null model.");
180 }
181
182 if (inSelector == null) {
183 return inModel.getClass();
184 }
185 Accessor accessor = findTerminalAccessor(inModel, inSelector);
186 if (accessor.descriptor == null) {
187 // List
188 return Object.class;
189 // TODO: should test type of Object[]
190 } else if (accessor.isGetterIndexed) {
191 // Indexed getter
192 Method getter = ((IndexedPropertyDescriptor) accessor.descriptor).getIndexedReadMethod();
193 if (getter == null) {
194 throw new IllegalArgumentException("No indexed getter for: " + inSelector + " in model " + inModel);
195 }
196 return getter.getReturnType();
197 } else {
198 // Normal getter
199 Method getter = accessor.descriptor.getReadMethod();
200 if (getter == null) {
201 throw new IllegalArgumentException("No getter for: " + inSelector + " in model " + inModel);
202 }
203 return getter.getReturnType();
204 }
205 }
206
207
208 /***
209 * <P>
210 *
211 * Return an Iterator that iterates over Selectors for all properties of the
212 * passed model object. </P>
213 *
214 * @param inModel model to make an Iterator for.
215 * @return Iterator that iterates over Selectors for all properties of the
216 * passed model object.
217 */
218 public Iterator getSelectorIterator(Object inModel) {
219 if (inModel == null) {
220 throw new IllegalArgumentException("Can't getSelectorIterator for a null model.");
221 }
222
223 if (inModel instanceof Object[]) {
224 return new IntIndexSelectorIterator(0, ((Object[]) inModel).length - 1);
225 }
226
227 PropertyDescriptor[] descriptors = BeanInfos.getBeanInfo(inModel.getClass()).getPropertyDescriptors();
228 Selector[] selectors = new Selector[descriptors.length];
229 for (int i = 0; i < descriptors.length; ++i) {
230 selectors[i] = Selector.fromString(descriptors[i].getName());
231 // TODO: cache these!
232 }
233 Iterator result = new ArraySelectorIterator(selectors);
234
235 if (inModel instanceof List) {
236 return new CompoundSelectorIterator(result, new IntIndexSelectorIterator(0, ((List) inModel).size() - 1));
237 } else {
238 return result;
239 }
240 }
241
242
243 /***
244 * <P>
245 *
246 * Tries to find a Selector that would get() a property equals() to the
247 * passed Object. If the model is a java.util.List, call indexOf() to search
248 * contents first. For Object[] search through contents. Else get an
249 * Iterator over all properties and iterate over to find a match. </P> <P>
250 *
251 * Note: doesn't search through JavaBeans indexed properties for a match.
252 * </P>
253 *
254 * @param inProperty the property Object to find.
255 * @param inModel the model to get the property from.
256 * @return a Selector that would return an Object equals() to the passed
257 * property, or null if not found.
258 */
259 public Selector getSelectorFor(Object inModel, Object inProperty) {
260 if (inModel == null) {
261 throw new IllegalArgumentException("Can't getSelectorFor for a null model.");
262 }
263
264 if (inProperty == null) {
265 return null;
266 }
267
268 if (inModel instanceof java.util.List) {
269 int i = ((java.util.List) inModel).indexOf(inProperty);
270 if (i < 0) {
271 return null;
272 }
273 return Selector.fromInt(i);
274 }
275
276 if (inModel instanceof Object[]) {
277 Object[] array = (Object[]) inModel;
278 for (int i = array.length - 1; i >= 0; --i) {
279 Object o = array[i];
280 if (o != null && o.equals(inProperty)) {
281 return Selector.fromInt(i);
282 }
283 }
284 return null;
285 }
286
287 Iterator i = getSelectorIterator(inModel);
288 if (Debug.ON) {
289 Debug.assertTrue(i != null, "null Iterator");
290 }
291 while (i.hasNext()) {
292 Object o = i.next();
293 if (Debug.ON) {
294 Debug.assertTrue(o == null || o instanceof Selector, "Iterator doesn't contain Selector: " + o);
295 }
296 Selector s = (Selector) o;
297 try {
298 o = get(inModel, s);
299 if (o != null && o.equals(inProperty)) {
300 return s;
301 }
302 } catch (Exception e) {
303 // ignore that property and carry on
304 }
305 }
306 return null;
307 }
308
309
310 /***
311 * <P>
312 *
313 * Set the value of the property identified by a {@link Selector} in the
314 * passed model object to a new value. </P> <P>
315 *
316 * The implementation should not set the value if the new value has the same
317 * Object reference as the original. It could also avoid setting the value
318 * if the new value is equivalent to the old value, and the value is of an
319 * immutable Class (like Integer, String). Otherwise the property must be
320 * set to the new value, even if it equals() the old value. </P> <P>
321 *
322 * Usually, a {@link ModelChangeEvent} for {@link
323 * ModelChangeEvent#VALUE_CHANGED} should be broadcast by the model when the
324 * property is set so that interested listeners know that the model's state
325 * has changed. </P> <P>
326 *
327 * If the property is a sub-model object then the parent model should be
328 * registered as a {@link ModelChangeListener} to be able to propagate
329 * events properly. This propagation is partially implemented in {@link
330 * org.scopemvc.model.basic.BasicModel} but it relies on child Models being
331 * listened to by their parent. (Note: deregister from the old Model then
332 * register with the new one). See the sample code for examples using {@link
333 * org.scopemvc.model.basic.BasicModel#listenNewSubmodel} and {@link
334 * org.scopemvc.model.basic.BasicModel#unlistenOldSubmodel}. </P>
335 *
336 * @param inModel model to set the property on.
337 * @param inSelector identify the property to be set. Can't be null.
338 * @param inValue the value to set the property to.
339 * @throws Exception if the value could not be set in the model
340 * @todo Could add convenience methods for primitive types? (smefroy)
341 */
342 public void set(Object inModel, Selector inSelector, Object inValue)
343 throws Exception {
344
345 if (inModel == null) {
346 throw new IllegalArgumentException("Can't set for a null model.");
347 }
348
349 if (inSelector == null) {
350 throw new IllegalArgumentException("Can't set value of model (passed Selector is null)");
351 }
352
353 Accessor accessor;
354 try {
355 accessor = findTerminalAccessor(inModel, inSelector);
356 if (Debug.ON) {
357 Debug.assertTrue(accessor.selector != null, "null accessor.selector");
358 }
359 if (LOG.isDebugEnabled()) {
360 LOG.debug("set: accessor: " + accessor + ", value: " + inValue);
361 }
362 } catch (NullPropertyException e) {
363 throw new UnsupportedOperationException("Can't set property with selector: " + Selector.asString(inSelector) + " in model: " + inModel);
364 }
365
366 try {
367
368 if (accessor.descriptor == null) {
369 // java.util.List or Object[]
370 if (LOG.isDebugEnabled()) {
371 LOG.debug("set: int indexed property");
372 }
373 if (Debug.ON) {
374 Debug.assertTrue(accessor.selector instanceof IntIndexSelector);
375 }
376 int index = ((IntIndexSelector) accessor.selector).getIndex();
377 Object model = accessor.model;
378 if (model instanceof List) {
379 ((List) model).set(index, inValue);
380 } else if (model instanceof Object[]) {
381 ((Object[]) model)[index] = inValue;
382 } else {
383 throw new IllegalArgumentException("Can't access properties using int index (expected List or Object[]) with selector: " + Selector.asString(inSelector) + " in model: " + inModel);
384 }
385
386 } else if (accessor.isGetterIndexed) {
387 // Indexed setter
388 if (LOG.isDebugEnabled()) {
389 LOG.debug("set: indexed");
390 }
391 if (Debug.ON) {
392 Debug.assertTrue(accessor.descriptor instanceof IndexedPropertyDescriptor);
393 }
394 Method setter = ((IndexedPropertyDescriptor) accessor.descriptor).getIndexedWriteMethod();
395 if (setter == null) {
396 throw new IllegalArgumentException("Can't find indexed setter for selector: " + Selector.asString(inSelector) + " in model: " + inModel);
397 }
398 if (Debug.ON) {
399 Debug.assertTrue(accessor.selector instanceof StringIndexSelector);
400 }
401 if (Debug.ON) {
402 Debug.assertTrue(accessor.selector.getNext() instanceof IntIndexSelector, "not IntIndexSelector: " + accessor.selector);
403 }
404 Integer index = new Integer(((IntIndexSelector) accessor.selector.getNext()).getIndex());
405 Object[] params = {index, inValue};
406 setter.invoke(accessor.model, params);
407
408 } else {
409 // Normal setter
410 Method setter = ((PropertyDescriptor) accessor.descriptor).getWriteMethod();
411 if (LOG.isDebugEnabled()) {
412 LOG.debug("set: normal: " + setter);
413 }
414 if (setter == null) {
415 throw new IllegalArgumentException("Can't find setter for selector: " + Selector.asString(inSelector) + " in model: " + inModel);
416 }
417 Object[] params = {inValue};
418 setter.invoke(accessor.model, params);
419 }
420
421 } catch (InvocationTargetException e) {
422 if (LOG.isDebugEnabled()) {
423 LOG.debug("set: can't invoke setter for selector: " + Selector.asString(inSelector) + " in model: " + inModel, e);
424 }
425 if (e.getTargetException() instanceof Exception) {
426 throw (Exception) e.getTargetException();
427 } else {
428 throw e;
429 }
430 } catch (IllegalAccessException e1) {
431 if (LOG.isDebugEnabled()) {
432 LOG.debug("set: no accessible setter for selector: " + Selector.asString(inSelector) + " in model: " + inModel, e1);
433 }
434 throw new IllegalArgumentException("Illegal access to setter for selector: " + Selector.asString(inSelector) + " in model: " + inModel);
435 } catch (IndexOutOfBoundsException e2) {
436 if (LOG.isDebugEnabled()) {
437 LOG.debug("set: IndexOutOfBoundsException: " + inSelector, e2);
438 }
439 throw new IllegalArgumentException("Can't access property for selector: " + Selector.asString(inSelector) + " in model: " + inModel);
440 } catch (NullPointerException e3) {
441 if (LOG.isDebugEnabled()) {
442 LOG.debug("set: NullPointerException: " + inSelector, e3);
443 }
444 throw new IllegalArgumentException("Can't access property for selector: " + Selector.asString(inSelector) + " in model: " + inModel);
445 }
446 }
447
448
449 /***
450 * <P>
451 *
452 * Set the value of the property identified by a {@link Selector} in the
453 * passed model object to a new value. </P> <P>
454 *
455 * The implementation should not set the value if the new value has the same
456 * Object reference as the original. It could also avoid setting the value
457 * if the new value is equivalent to the old value, and the value is of an
458 * immutable Class (like Integer, String). Otherwise the property must be
459 * set to the new value, even if it equals() the old value. </P> <P>
460 *
461 * Usually, a {@link ModelChangeEvent} for {@link
462 * ModelChangeEvent#VALUE_CHANGED} should be broadcast by the model when the
463 * property is set so that interested listeners know that the model's state
464 * has changed. </P> <P>
465 *
466 * If the property is a sub-model object then the parent model should be
467 * registered as a {@link ModelChangeListener} to be able to propagate
468 * events properly. This propagation is partially implemented in {@link
469 * org.scopemvc.model.basic.BasicModel} but it relies on child Models being
470 * listened to by their parent. (Note: deregister from the old Model then
471 * register with the new one). See the sample code for examples using {@link
472 * org.scopemvc.model.basic.BasicModel#listenNewSubmodel} and {@link
473 * org.scopemvc.model.basic.BasicModel#unlistenOldSubmodel}. </P>
474 *
475 * @param inModel model to set the property on.
476 * @param inSelector identify the property to be set. Can't be null.
477 * @return TODO: Describe the Return Value
478 * @throws Exception if the value could not be set in the model
479 */
480 public boolean hasProperty(Object inModel, Selector inSelector) {
481 if (inModel == null) {
482 throw new IllegalArgumentException("Can't ask hasProperty for a null model.");
483 }
484
485 if (inSelector == null) {
486 return true;
487 }
488 try {
489 Accessor accessor = findTerminalAccessor(inModel, inSelector);
490 accessor.traverseProperty();
491 return true;
492 } catch (Exception e) {
493 return false;
494 // ***** this is a bit nasty
495 }
496 }
497
498
499 // ------------------------ Working directly with JavaBeans properties ------------------------
500
501 /***
502 * Burrow down through submodels to find the terminal accessor (which could
503 * be a JavaBeans indexed property) from the passed model and selector.
504 *
505 * @param inModel model to get the accessor on.
506 * @param inSelector identify the property. Can't be null.
507 * @return The terminal accessor
508 * @throws Exception if the Accessor could not be found for any reason
509 */
510 Accessor findTerminalAccessor(Object inModel, Selector inSelector)
511 throws Exception {
512 if (LOG.isDebugEnabled()) {
513 LOG.debug("findTerminalAccessor: " + inModel + ", " + inSelector);
514 }
515 if (Debug.ON) {
516 Debug.assertTrue(inModel != null, "null model");
517 }
518 if (Debug.ON) {
519 Debug.assertTrue(inSelector != null, "null Selector");
520 }
521
522 Accessor accessor = new Accessor(inModel, inSelector);
523 accessor.findProperty();
524 while (!accessor.isAtTerminal()) {
525 if (LOG.isDebugEnabled()) {
526 LOG.debug("findTerminalAccessor: not at terminal: " + accessor.selector);
527 }
528 accessor.traverseProperty();
529 accessor.findProperty();
530 }
531 if (LOG.isDebugEnabled()) {
532 LOG.debug("findTerminalAccessor: found terminal: " + accessor.selector);
533 }
534 return accessor;
535 }
536
537 }
This page was automatically generated by Maven